<?php
declare(strict_types=1);


namespace txz\common\third_lib\model\lib;

use ErrorException;
use PDO;
use PDOException;
use PDOStatement;

/**
 * Class PDOManager
 * @package txz\common\third_lib\model\lib
 */
class PDOManager implements Dao
{
    //pdo对象
    private $_pdo = null;
    private $logDir = null;
    private $sql = '';

    /**
     * PDOManager constructor.
     * @param string|PDO $host
     * @param string $port
     * @param string $dbName
     * @param string $user
     * @param string $pwd
     * @param string $logDir
     * @throws ErrorException
     */
    public function __construct($host = '', $port = '', $dbName = '', $user = '', $pwd = '', $logDir = '')
    {
        if ($host && $port && $dbName && $user && $pwd) {
            try {
                if (!$logDir)
                    $this->logDir = ROOT_DIR . 'log/db.log';
                $this->setPdo(new PDO('mysql:host=' . $host . ';dbname=' . $dbName, $user, $pwd, [//                PDO::MYSQL_ATTR_INIT_COMMAND => "set names utf8mb4"
                ]));

                $this->getPdo()->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);

            } catch (PDOException $e) {
                throw new ErrorException($e->getMessage());
            }
        } else {
            $this->setPdo($host);
            $host->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
            $host->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, false);
        }
    }

    /**
     * @param $_sql
     * @return bool
     * @throws ErrorException
     */
    public function execute($_sql)
    {
        try {
            $this->sql = $_sql;
            $_stmt = $this->getPdo()->prepare($_sql);
            return $_stmt->execute();
        } catch (PDOException  $e) {
            throw new ErrorException('SQL语句：' . $_sql . '<br />错误信息：' . $e->getMessage());
        }
    }

    /**
     * @param $where
     * @return bool|string
     */
    private function makeWhere(&$where)
    {
        $count = 65;
        if (is_array($where) && $where) {
            $str = 'WHERE ';
            foreach ($where as $k => $v) {
                if (is_array($v)) {
                    $fuhao = $v[0];
                    $v = $v[1];
                } else {
                    $fuhao = '=';
                }
                $tmp = ':a' . chr($count++);
                $str .= "`$k`".$fuhao.$tmp." AND ";
                $where[$tmp] = $v;
                unset($where[$k]);
            }
            $str = substr($str, 0, -5);
            return $str;
        } else {
            return '';
        }
    }

    /**
     * @param $need
     * @return bool|string
     */
    private function makeNeed($need)
    {
        if (is_array($need) && $need) {
            $str = '';
            foreach ($need as $k => $v) {
                $str .= "`$v`,";
            }
            $str = substr($str, 0, -1);
            return $str;
        } else {
            return '*';
        }
    }

    /**
     * @param $group
     * @return string
     */
    public function makeGroup($group)
    {
        if ($group)
            return "GROUP BY $group";
        else
            return '';
    }

    /**
     * @param $limit
     * @return string
     */
    public function makeLimit($limit)
    {
        if ($limit && is_array($limit))
            return "LIMIT " . $limit[0] . "," . $limit[1];
        else
            return '';
    }

    /**
     * @param $data
     * @return array|string
     */
    private function makeSetData($data)
    {
        $count = 65;
        if (is_object($data)){
            $data = json_decode(json_encode($data), true);
        }
        if ($data && is_array($data)) {
            $str = 'SET ';
            foreach ($data as $k => $v) {
                if ($k == 'id') {
                    unset($data[$k]);
                    continue;
                }
                $tmp = ":b" . chr($count++);
                $str .= "`{$k}`=$tmp,";
                $data[$tmp] = $v;
                unset($data[$k]);
            }
            $str = substr($str, 0, -1);
            return [$str, $data];
        } else {
            return '';
        }
    }

    /**
     * @param PDOStatement $stmt
     * @param $data
     */
    private function bindParams(PDOStatement $stmt, $data)
    {
        if (is_array($data)) {
            foreach ($data as $k => $v) {
                if (is_array($v)) {
                    foreach ($v as $key => $val) {
                        $stmt->bindValue($key, $val);
                    }
                } else {
                    $stmt->bindValue($k, $v);
                }
            }
        }
    }

    /**
     * @param string $tableName
     * @param string $need
     * @param string $where
     * @param string $group
     * @param string $sort
     * @return array|mixed
     * @throws ErrorException
     */
    public function get_obj($tableName = '', $need = '', $where = '', $group = '', $sort = '')
    {
        try {
            if (!$tableName)
                throw new ErrorException('no table');

            $str = $this->makeWhere($where);
            if (!$str)
                throw new ErrorException('cant find');

            $need = $this->makeNeed($need);
            $group = $this->makeGroup($group);
            $sort = $this->makeOrder($sort);
            $this->sql = "SELECT $need FROM `$tableName` {$str} $group {$sort}";
//            var_dump($sql,$where);exit;

            $_stmt = $this->getPdo()->prepare($this->sql);
            if (is_array($where)) {
                $this->bindParams($_stmt, $where);
            }
            $_stmt->execute();

            $res = $_stmt->fetch(PDO::FETCH_ASSOC);

            if (!is_array($res) || !$res) {
                return [];
            } else {
                return $res;
            }
        } catch (PDOException  $e) {
            throw new ErrorException($e->getMessage());
        } catch (ErrorException $e) {
            throw new ErrorException($e->getMessage());
        }
    }

    /**
     * @param null $tableName
     * @param null $where
     * @return int
     * @throws ErrorException
     */
    public function get_obj_count($tableName = null, $where = null)
    {
        try {
            if (!$tableName)
                throw new ErrorException('no table');

            $str = $this->makeWhere($where);
            $this->sql = "SELECT count(*) FROM `$tableName` $str";
//            var_dump($sql,$where);exit;
            $_stmt = $this->getPdo()->prepare($this->sql);
            $this->bindParams($_stmt, $where);
            $_stmt->execute();
            $res = $_stmt->fetch(PDO::FETCH_ASSOC);
            if (!is_array($res) || !$res) {
                return 0;
            } else {
                return array_values($res)[0];
            }
        } catch (PDOException  $e) {
            throw new ErrorException($e->getMessage());
        } catch (ErrorException $e) {
            throw new ErrorException($e->getMessage());
        }
    }

    /**
     * @param $sort
     * @return string
     */
    private function makeOrder($sort)
    {
        if ($sort) {
            if (is_array($sort)) {
                $sort = array_values($sort);
                return "ORDER BY `{$sort[0]}` ASC";
            } else {
                return "ORDER BY `{$sort}` DESC";
            }
        }
        return '';
    }

    /**
     * @param string $tableName
     * @param string $need
     * @param string $where
     * @param string $group
     * @param string $sort
     * @param string $limit
     * @return array
     * @throws ErrorException
     */
    public function get_obj_arr($tableName = '', $need = '', $where = '',
                                $group = '', $sort = '',
                                $limit = '')
    {
        try {
            if (!$tableName)
                throw new ErrorException('no table');

            $str = $this->makeWhere($where);
            $group = $this->makeGroup($group);
            $sort = $this->makeOrder($sort);
            $limit = $this->makeLimit($limit);
            $need = $this->makeNeed($need);
            $this->sql = "SELECT $need FROM `$tableName` {$str} $group {$sort} $limit";
            $_stmt = $this->getPdo()->prepare($this->sql);
            if (is_array($where)) {
                $this->bindParams($_stmt, $where);
                unset($where);
            }
            $_stmt->execute();
            $res = $_stmt->fetchAll(PDO::FETCH_ASSOC);

            if (!is_array($res) || !$res) {
                return [];
            } else {
                return $res;
            }
        } catch (PDOException  $e) {
            throw new ErrorException($e->getMessage());
        } catch (ErrorException $e) {
            throw new ErrorException($e->getMessage());
        }
    }

    /**
     * @param string $tableName
     * @param string $data
     * @return bool
     * @throws ErrorException
     */
    public function add_obj($tableName = '', $data = '')
    {
        try {
            if (!$tableName)
                throw new ErrorException('no table');

            $arr = $this->makeSetData($data);
            $insert = $arr[0];
            $data = $arr[1];

            $this->sql = "INSERT INTO `$tableName` $insert";

//            var_dump($sql, $data);exit;

            $_stmt = $this->getPdo()->prepare($this->sql);
            $this->bindParams($_stmt, $data);
            $ret = $_stmt->execute();
            if (!$ret && $this->logDir) {
                file_put_contents($this->logDir,
                    json_encode(["time" => date("Y-m-d H:i:s"),
                        "dberr_insert" => $_stmt->errorInfo(), "data" => $data,
                        "sql" => $this->sql,
                        "pdo_error" => $this->getPdo()->errorInfo(),
                    ]) . "\n\n", FILE_APPEND);
            }
            return $ret;
        } catch (PDOException  $e) {
            throw new ErrorException($e->getMessage());
        } catch (ErrorException $e) {
            throw new ErrorException($e->getMessage());
        }
    }

    /**
     * @param string $tableName
     * @param string $data
     * @return bool
     * @throws ErrorException
     */
    public function replace_obj($tableName = '', $data = '')
    {
        try {
            if (!$tableName)
                throw new ErrorException('no table');

            $arr = $this->makeSetData($data);
            $insert = $arr[0];
            $data = $arr[1];

            $this->sql = "REPLACE INTO `$tableName` $insert";

//            var_dump($sql, $data);exit;

            $_stmt = $this->getPdo()->prepare($this->sql);
            $this->bindParams($_stmt, $data);
            $ret = $_stmt->execute();
            if (!$ret && $this->logDir) {
                file_put_contents($this->logDir,
                    json_encode(["time" => date("Y-m-d H:i:s"),
                        "dberr_insert" => $_stmt->errorInfo(), "data" => $data,
                        "sql" => $this->sql,
                        "pdo_error" => $this->getPdo()->errorInfo(),
                    ]) . "\n\n", FILE_APPEND);
            }
            return $ret;
        } catch (PDOException  $e) {
            throw new ErrorException($e->getMessage());
        } catch (ErrorException $e) {
            throw new ErrorException($e->getMessage());
        }
    }

    /**
     * @param string $tableName
     * @param string $where
     * @return bool
     * @throws ErrorException
     */
    public function remove_obj($tableName = '', $where = '')
    {
        try {
            if (!$tableName)
                throw new ErrorException('no table');

            $str = $this->makeWhere($where);
            if (!$str)
                throw new ErrorException('cant remove');

            $this->sql = "DELETE FROM `$tableName` {$str}";
            $_stmt = $this->getPdo()->prepare($this->sql);
            $this->bindParams($_stmt, [$where]);

            $ret = $_stmt->execute();
            if (!$ret && $this->logDir) {
                file_put_contents($this->logDir,
                    json_encode(["time" => date("Y-m-d H:i:s"),
                        "dberr_delete" => $_stmt->errorInfo(), "data" => [$where],
                        "sql" => $this->sql,
                        "pdo_error" => $this->getPdo()->errorInfo(),
                    ]) . "\n\n", FILE_APPEND);
            }
            return $ret;
        } catch (PDOException  $e) {
            throw new ErrorException($e->getMessage());
        } catch (ErrorException $e) {
            throw new ErrorException($e->getMessage());
        }
    }

    /**
     * @param string $tableName
     * @param array $data
     * @param array $where
     * @return bool
     * @throws ErrorException
     */
    public function update_obj($tableName = '', $data = [], $where = [])
    {
        try {
            if (!$tableName)
                throw new ErrorException('no table');

            if (!$data)
                throw new ErrorException('no need to update');

            $str = $this->makeWhere($where);
            if (!$str)
                throw new ErrorException('cant update');

            $arr = $this->makeSetData($data);
            $set = $arr[0];
            $data = $arr[1];

            $this->sql = "UPDATE $tableName $set $str";
            $_stmt = $this->getPdo()->prepare($this->sql);
            $this->bindParams($_stmt, [$data, $where]);
            $ret = $_stmt->execute();
            if (!$ret && $this->logDir) {
                file_put_contents($this->logDir,
                    json_encode(["time" => date("Y-m-d H:i:s"),
                        "dblog_update" => [$data, $where],
                        "sql" => $this->sql,
                        "pdo_error" => $this->getPdo()->errorInfo(),
                    ]) . "\n\n", FILE_APPEND);
            }
            return $ret;
        } catch (PDOException  $e) {
            throw new ErrorException($e->getMessage());
        } catch (ErrorException $e) {
            throw new ErrorException($e->getMessage());
        }
    }

    /**
     * @param $sql
     * @return mixed
     * @throws ErrorException
     */
    public function get_obj_by_sql($sql)
    {
        try {
            $this->sql = $sql;
            $_stmt = $this->getPdo()->prepare($this->sql);
            $_stmt->execute();
            return $_stmt->fetch(PDO::FETCH_ASSOC);
        } catch (ErrorException $e) {
            throw new ErrorException($e->getMessage());
        }
    }

    /**
     * @param $sql
     * @return array
     * @throws ErrorException
     */
    public function get_obj_arr_by_sql($sql)
    {
        try {
            $this->sql = $sql;
            $_stmt = $this->getPdo()->prepare($this->sql);
            $_stmt->execute();
            return $_stmt->fetchAll(PDO::FETCH_ASSOC);
        } catch (ErrorException $e) {
            throw new ErrorException($e->getMessage());
        }
    }

    /**
     * @return PDO
     */
    public function getPdo()
    {
        return $this->_pdo;
    }

    /**
     * @param PDO $pdo
     */
    public function setPdo($pdo)
    {
        $this->_pdo = $pdo;
    }

    /**
     * @return string
     */
    public function get_last_sql()
    {
        return $this->sql;
    }

    /**
     * @return int
     */
    public function get_last_insert_id(): int
    {
        return (int)$this->getPdo()->lastInsertId();
    }

    /**
     * @param $call_back
     * @return bool
     */
    public function transaction($call_back): bool
    {
        $this->getPdo()->beginTransaction();
        try{
            $arr = $call_back();
            foreach ($arr as $res){
                if (!$res){
                    throw new PDOException();
                }
            }
            $this->getPdo()->commit();
            return true;
        }catch (PDOException $e){
            $this->getPdo()->rollBack();
        }
        return false;
    }
}